﻿// ---------------------------------------------------------------------------------------------------------------------
//   Copyright (c) SRT Solutions 2009. All rights reserved.
// ---------------------------------------------------------------------------------------------------------------------

using System;
using System.IO;

using NUnit.Framework;

using SRTSolutions.Elevate;

namespace SRTSolutions.Elevate.Guide
{
    /// <summary>
    /// Describes the Option type. Option types are used to describe calculaitons that may or may not return a value.
    /// </summary>
    public class OptionType
    {
        [Test]
        public void OptionTypesCanContainValues()
        {
            //given a value
            var value = 10;

            //we can wrap it in an option type like this
            Option<int> option = Option.Some(value);

            Assert.IsTrue(option.IsSome);
            Assert.IsFalse(option.IsNone);
            Assert.AreEqual(10, option.Value);
        }

        [Test]
        public void OptionTypesDontHaveToContainvalues()
        {
            //given an option type created with "None"
            var option = Option<Stream>.None;

            //we can see that it contains no value - it is set to "None" instead of "Some"
            Assert.IsTrue(option.IsNone);
            Assert.IsFalse(option.IsSome);
            Assert.Throws<InvalidOperationException>(() => { var tmp = option.Value; });
        }

        [Test]
        public void UsingOptionTypesPractically()
        {
            //when reading data from files
            using (var stream = new MemoryStream())
            {
                var writer = new StreamWriter(stream);
                writer.Write('a');
                writer.Flush();

                stream.Position = 0;
                var reader = new StreamReader(stream);

                //the read function doesn't always return a result (it returns -1 if a valid character can't be read)
                Func<int> read = reader.Read;

                //we can use an option type to create a better design for this function
                Func<Option<char>> readWithOption =
                    () =>
                    {
                        var result = read();

                        if (result == -1)
                        {
                            return Option<char>.None;
                        }
                        else
                        {
                            return Option.Some((char)result);
                        }
                    };

                //the first read returns a result with the value 'a'
                Assert.AreEqual('a', readWithOption().Value);

                //but the second read returns none since it is at the end of the stream
                Assert.IsTrue(readWithOption().IsNone);
            }
        }

        [Test]
        public void DefaultValues()
        {
            //if you have an option type that is Some
            var someValue = Option.Some(124);

            //you can get the value like this
            var actualValue = someValue.GetValueOrDefaultTo(45);
            Assert.AreEqual(124, actualValue);

            //but if the option is None
            var noneValue = Option<int>.None;

            //you can provide a default
            var defaultValue = noneValue.GetValueOrDefaultTo(45);
            Assert.AreEqual(45, defaultValue);
        }

        [Test]
        public void MultipleOpertionsWithOptionTypes()
        {
            //say we have a few functions that may or may not return a value.
            Func<int, Option<int>> divideIfEven = value => ((value % 2) == 0) ? Option.Some(value / 2) : Option<int>.None;
            Func<int, Option<int>> subtractIfDivisibleByThree = value => ((value % 3) == 0) ? Option.Some(value - 3) : Option<int>.None;
            Func<int, Option<int>> multiplyIfOdd = value => ((value % 2) != 0) ? Option.Some(value * 2) : Option<int>.None;

            //we can chain these operations together like this:
            Option<int> result =
                divideIfEven(36)
                .Select(subtractIfDivisibleByThree)
                .Select(multiplyIfOdd);


            //the result of one carries on to the next to yield the expected result
            Assert.IsTrue(result.IsSome);
            Assert.AreEqual(30, result.Value);
        }

        [Test]
        public void MultipeOperationsFailing()
        {
             //say we have a few functions that may or may not return a value.
            Func<int, Option<int>> divideIfEven = value => ((value % 2) == 0) ? Option.Some(value / 2) : Option<int>.None;
            Func<int, Option<int>> multiplyIfOdd = value => ((value % 2) != 0) ? Option.Some(value * 2) : Option<int>.None;
            Func<int, Option<int>> subtractIfDivisibleByThree = value => ((value % 3) == 0) ? Option.Some(value - 3) : Option<int>.None;

            //if one operation in the chain fails...
            Option<int> result =
                divideIfEven(36)
                .Select(multiplyIfOdd) //this will fail
                .Select(subtractIfDivisibleByThree);


            //the result will be None
            Assert.IsTrue(result.IsNone);
        }

        #region Unit Tests

        [Test]
        public void InequalitySomeNone()
        {
            var one = Option.Some(1);
            var none = Option<int>.None;

            Assert.AreNotEqual(one, none);
        }

        [Test]
        public void InequalitySomeSome()
        {
            var one = Option.Some(1);
            var two = Option.Some(2);

            Assert.AreNotEqual(one, two);
        }

        [Test]
        public void EqualitySomeSome()
        {
            var one1 = Option.Some(1);
            var one2 = Option.Some(1);

            Assert.AreEqual(one1, one2);
        }

        [Test]
        public void EqualityNoneNone()
        {
            var none1 = Option<int>.None;
            var none2 = Option<int>.None;

            Assert.AreEqual(none1, none2);
        }

        [Test]
        public void ChainingOptionsThisException()
        {
            Option<int> tmp = null;

            Assert.Throws<ArgumentNullException>(() => tmp.Select(value => Option.Some(value++)));

        }

        [Test]
        public void ChainingOptionsArgumentException()
        {
            var tmp = Option.Some(1);

            Assert.Throws<ArgumentNullException>(() => tmp.Select<int, int>(null));

        }

        [Test]
        public void GetValueOrDefaultToThisException()
        {
            Option<int> tmp = null;
            Assert.Throws<ArgumentNullException>(() => tmp.GetValueOrDefaultTo(123));
        }

        [Test]
        public void GetValueOrDefaultToArgumentException()
        {
            var tmp = Option.Some("abcd");
            Assert.Throws<ArgumentNullException>(() => tmp.GetValueOrDefaultTo(null));
        }
        #endregion
    }
}
